/**************************************************************************************
 * Date 		By				Description
 * ------------------------------------------------------------------------------------
 * 03-20-07		Noah V			Created
 *
 **************************************************************************************/

using System;
using System.Diagnostics;
using System.Web;
using System.Web.Caching;
using System.Collections;
using System.Reflection;

namespace NAFx.Base
{
    internal enum CacheType
    {
        NoCache = 0,
        Sliding,
        Absolute
    }

    /// <remarks>
    /// Information regarding a SmartObject type.
    /// config entry format: shift,timeout,type
    /// </remarks>
    class SmartObjectInfo
    {
        /// <summary>
        /// The prototype object, used to create an new instance
        /// </summary>
        public ISmartObject prototype;

        /// <summary>
        /// The prefix for the cache key
        /// </summary>
        internal string cacheIDPrefix;

        /// <summary>
        /// The callback when instance is removed from cache
        /// </summary>
        internal CacheItemRemovedCallback callback;

        /// <summary>
        /// The relative shift applied to priority
        /// </summary>
        internal int priorityShift;

        /// <summary>
        /// The timeout value
        /// </summary>
        internal TimeSpan cacheTimeout;

        /// <summary>
        /// The type of cache
        /// </summary>
        internal CacheType type;

        /// <summary>
        /// whether to set cache dependency
        /// </summary>
        internal bool refresh;

        /// <summary>
        /// whether to reuse an expired object temporarily
        /// </summary>
        internal bool reuse;

        /// <summary>
        /// The fallback delegate, null if no fallback is needed
        /// </summary>
        internal FallbackDelegate fallbackDelegate;
    }

    /// <remarks>
    /// Both a cache manager and an object factory for SmartObject.
    /// It is global: all members are static.
    /// </remarks>
    public static class SmartObjectManager
    {
        private static Cache cache = HttpRuntime.Cache;
        private static SmartObjectInfo[] typeList;
        private static Hashtable typeTable = new Hashtable();

        /// <summary>
        /// Init the SmartObjectManager.  Shall be called before anything else
        /// can be used.
        /// </summary>
        /// <returns>whether the initialization is successful</returns>
        public static bool Init()
        {
            ArrayList list = new ArrayList(40);

            Assembly assembly = Assembly.GetExecutingAssembly();
            if (!processAssembly(assembly, list))
                return false;

            string externalAssembly = Utility.ConfigValue("ExternalAssembly");
            if (!Utility.IsEmpty(externalAssembly))
            {
                string[] assemblys = externalAssembly.Split('|');
                for (int i = 0; i < assemblys.Length; i++)
                {
                    assembly = Utility.LoadAssemblyWithPartialName(assemblys[i], true);
                    if (!processAssembly(assembly, list))
                        return false;
                }
            }

            // convert ArrayList to an array will make it slightly
            // more efficient to be used
            typeList = new SmartObjectInfo[list.Count];
            for (int i = 0; i < list.Count; i++)
                typeList[i] = (SmartObjectInfo)list[i];

            return true;
        }

        /// <summary>
        /// Allows the application to set fallback policy of a SmartObject type
        /// </summary>
        /// <param name="typeID">the type to set fallback policy</param>
        /// <param name="policy">the fallback delegate implementing the policy</param>
        public static void SetFallbackPolicy(int typeID, FallbackDelegate policy)
        {
            SmartObjectInfo info = typeList[typeID];
            info.fallbackDelegate = policy;
        }

        private static bool processAssembly(Assembly assembly, ArrayList list)
        {
            Module[] modules = assembly.GetModules();
            for (int i = 0; i < modules.Length; i++)
            {
                Type[] types = modules[i].GetTypes();
                for (int j = 0; j < types.Length; j++)
                {
                    if (typeof(ISmartObject).IsAssignableFrom(types[j]) && types[j].IsClass && !types[j].IsAbstract)
                    {
                        if (!processType(types[j], list))
                            return false;
                    }
                }
            }

            return true;
        }

        private static bool parsePolicy(string cachePolicy, SmartObjectInfo info)
        {
            try
            {
                string[] tmp = cachePolicy.Split(',');
                info.priorityShift = Utility.ToInt(tmp[0]);
                info.cacheTimeout = TimeSpan.FromSeconds(Utility.ToInt(tmp[1]));
                info.type = (CacheType)Utility.ToInt(tmp[2]);
                info.refresh = (Utility.ToInt(tmp[3]) > 0);
                info.reuse = (tmp.Length >= 5 && Utility.ToInt(tmp[4]) == 1);
                if (info.reuse || (info.refresh && (info.prototype is ICacheStatus)))
                    info.callback = new CacheItemRemovedCallback(RemovedCallback);
                else
                    info.callback = null;
            }
            catch (Exception e)
            {
                EventLogWriter.WriteException("SmartObjectManager", "cache not configured properly for type: " + info.prototype.GetType(), e);
                return false;
            }

            return true;
        }

        private static bool processType(Type type, ArrayList list)
        {
            // set the typeID
            try
            {
                type.InvokeMember("TypeID", BindingFlags.Public | BindingFlags.SetField | BindingFlags.Static, null, null, new object[] { list.Count });
            }
            catch
            {
                EventLogWriter.WriteEntry("SmartObjectManager", "unable to set typeid for type: " + type);
                return false;
            }

            SmartObjectInfo info = new SmartObjectInfo();
            info.prototype = (ISmartObject)Activator.CreateInstance(type, false);
            if (info.prototype is IFallbackPolicy)
                info.fallbackDelegate = ((IFallbackPolicy)info.prototype).FallbackPolicy;
            else
                info.fallbackDelegate = null;

            info.cacheIDPrefix = list.Count + ":";

            string typeName = null;
            string cachePolicy = null;
            do
            {
                if (typeof(ISmartObject).IsAssignableFrom(type))
                {
                    string name = type.ToString();
                    name = name.Substring(name.LastIndexOf(".") + 1);

                    if (typeName == null)
                    {
                        typeName = name;
#if	DEBUG
                        info.cacheIDPrefix = typeName + ":";
#endif
                    }

                    cachePolicy = Utility.ConfigValue("cache." + name);
                    type = type.BaseType;
                }
                else
                    cachePolicy = Utility.ConfigValue("cache.default", "0,3600,1,1");
            } while (cachePolicy == null);

            if (!parsePolicy(cachePolicy, info))
                return false;

            typeTable[typeName] = list.Count;
            list.Add(info);

            return true;
        }

        /// <summary>
        /// Get the typeID for an ISmartObject type
        /// </summary>
        /// <param name="name">class name of the ISmartObject (without namespace)</param>
        /// <returns>the type ID, -1 if not found</returns>
        public static int GetTypeID(string name)
        {
            object index = typeTable[name];
            if (index == null)
                return -1;

            return (int)index;
        }

        /// <summary>
        /// Get the cache key for a given ISmartObject type and its id
        /// </summary>
        /// <param name="typeID">type id</param>
        /// <param name="id">id</param>
        /// <returns>cache key</returns>
        private static string GetCacheKey(int typeID, string id)
        {
            return typeList[typeID].cacheIDPrefix + id;
        }

        /// <summary>
        /// Get an object managed by the manager.  If it already exists in cache,
        /// the stored instance will be returned.  Otherwise a new instance will
        /// be created and inserted into cache.
        /// </summary>
        /// <param name="type">fully qualified class name of the smart object type</param>
        /// <param name="id">id of the object</param>
        /// <returns>an instance of the object requested.  null if failed</returns>
        public static ISmartObject GetSmartObject(string type, string id)
        {
            int typeID = GetTypeID(type);
            if (typeID < 0)
            {
                EventLogWriter.WriteEntry("SmartObjectManager", "Invalid type specified: " + type);
                return null;
            }

            return GetSmartObject(typeID, id);
        }

        /// <summary>
        /// Get an object managed by the manager.  If it already exists in cache,
        /// the stored instance will be returned.  Otherwise a new instance will
        /// be created and inserted into cache.
        /// </summary>
        /// <param name="typeID">type id of the object</param>
        /// <param name="id">id of the object</param>
        /// <returns>an instance of the object requested.  null if failed</returns>
        public static ISmartObject GetSmartObject(int typeID, string id)
        {
            string cacheKey = GetCacheKey(typeID, id);
            object result = cache[cacheKey];

            if (result == null)
            {
                try
                {
                    result = create(typeID, id);
                }
                catch (Exception e)
                {
                    EventLogWriter.WriteException("SmartObjectManager", "Exception caught when creating: " + id, e);
                }
            }

            return result as ISmartObject;
        }

        private static ISmartObject create(int typeID, string id)
        {
            // create an empty instance first
            SmartObjectInfo info = typeList[typeID];
            ISmartObject result = (ISmartObject)info.prototype.CreateInstance();
            string requestKey = GetCacheKey(typeID, id);

            string key = requestKey;
            string[] fallbacks = null;
            int fallbackIndex = 0;
            bool done = false;
            LoadResult r;

            do
            {
                r = result.Load(id);
                if (r != LoadResult.Failure)
                    done = true;
                else
                {
                    // lazy instantiation for fallback list
                    if (fallbacks == null && info.fallbackDelegate != null)
                        fallbacks = info.fallbackDelegate(id);

                    // if no fallback or fallback all tried out
                    if (fallbacks == null || fallbackIndex == fallbacks.Length)
                        return null;

                    id = fallbacks[fallbackIndex++];

                    // check cache
                    key = GetCacheKey(typeID, id);
                    object val = cache[key];
                    if (val != null)
                    {
                        done = true;

#if	DEBUG
                        EventLogWriter.WriteEntry("SmartObjectManager", "Fallback Cache hit: " + key, EventLogEntryType.Information);
#endif

                        result = (ISmartObject)val;
                    }
                }
            } while (!done);

            if (info.type == CacheType.NoCache || r == LoadResult.NoCache)
                return result;	// simply return if cache disabled

            string[] dependency;
            if (info.refresh)
                dependency = result.Dependency;
            else
                dependency = null;

            DateTime absoluteTimeout;
            TimeSpan slidingTimeout;
            if (info.type == CacheType.Absolute)
            {
                absoluteTimeout = DateTime.Now.Add(info.cacheTimeout);
                slidingTimeout = Cache.NoSlidingExpiration;
            }
            else
            {
                absoluteTimeout = Cache.NoAbsoluteExpiration;
                slidingTimeout = info.cacheTimeout;
            }

            CacheItemPriority priority;
            if (r != LoadResult.Failure)	// object just created
                priority = (CacheItemPriority)r;
            else
                priority = CacheItemPriority.Normal;	// for fallback object created early, priority can only be assumed normal

            priority = (CacheItemPriority)((int)priority + info.priorityShift);
            if (priority < CacheItemPriority.Low)
                return result;

            if (priority > CacheItemPriority.NotRemovable)
                priority = CacheItemPriority.NotRemovable;

            CacheDependency cacheDependency;
            if (dependency != null)
                cacheDependency = new CacheDependency(dependency);
            else
                cacheDependency = null;

            cache.Insert(key, result, cacheDependency, absoluteTimeout, slidingTimeout, priority, info.callback);

#if	DEBUG
            EventLogWriter.WriteEntry("SmartObjectManager", key + " inserted into cache", EventLogEntryType.Information);
#endif

            //	insert into cache for the requested id
            if (fallbackIndex > 0)
            {
                // a CacheDependency instance can only be used once, so have to recreate
                if (dependency != null)
                    cacheDependency = new CacheDependency(dependency);

                cache.Insert(requestKey, result, cacheDependency, absoluteTimeout, slidingTimeout, priority, info.callback);
#if	DEBUG
                EventLogWriter.WriteEntry("SmartObjectManager", requestKey + " fallback inserted into cache", EventLogEntryType.Information);
#endif
            }

            return result;
        }

        /// <summary>
        /// The callback used to set dirty flag when item removed from cache
        /// </summary>
        /// <param name="key">key</param>
        /// <param name="val">the object to be removed</param>
        /// <param name="reason">reason</param>
        public static void RemovedCallback(String key, Object val, CacheItemRemovedReason reason)
        {
            int index = key.IndexOf(':');
            int typeID;

#if	DEBUG
            string prefix = key.Substring(0, index + 1);
            for (typeID = 0; typeID < typeList.Length && typeList[typeID].cacheIDPrefix != prefix; typeID++) ;
#else
			typeID = Utility.ToInt(key.Substring(0, index));
#endif

            if (typeList[typeID].reuse)
            {
                cache.Insert(key, val);
#if DEBUG
                EventLogWriter.WriteEntry("SmartObjectManager", "instance reused: " + key, EventLogEntryType.Information);
#endif
                object newInstance = null;
                try
                {
                    newInstance = create(typeID, key.Substring(index + 1));
                }
                catch (Exception e)
                {
                    EventLogWriter.WriteException("SmartObjectManager", "Exception caught when re-creating: " + key, e);
                }
                if (newInstance == null)
                    cache.Remove(key);	// if fail remove the reused entry so that the next access start clean
            }

            ICacheStatus obj = val as ICacheStatus;
            if (obj != null)
                obj.IsDirty = true;
        }
    }
}
